Skip to content

fix(processor): dead-letter pubsub messages whose blob is persistently missing#3041

Open
dgellman wants to merge 2 commits into
guacsec:mainfrom
dgellman:fix/blob-nosuchkey-deadletter
Open

fix(processor): dead-letter pubsub messages whose blob is persistently missing#3041
dgellman wants to merge 2 commits into
guacsec:mainfrom
dgellman:fix/blob-nosuchkey-deadletter

Conversation

@dgellman

@dgellman dgellman commented May 7, 2026

Copy link
Copy Markdown

Summary

The processor previously retried blob NotFound responses indefinitely. process.Subscribe returned nil from its handler without acking, so NATS redelivered the same orphaned message forever — generating hundreds of thousands of GetObject errors per processor over weeks for blobs that no longer exist.

After maxBlobNotFoundRetries (3) consecutive NotFound responses for a message, the message is now acked to stop redelivery. Other read errors retain their previous redelivery behavior.

How retries are counted

When the underlying transport exposes a delivery count, that is preferred so the threshold is shared across replicas and survives processor restarts:

  • NATS JetStream: uses Metadata().NumDelivered via gocloud.dev/pubsub.Message.As(&jetstream.Msg).
  • Other backends (incl. mempubsub in tests): falls back to a per-key in-process counter.

A Prometheus counter guac_processor_blob_notfound_dropped_total (label processor_id) is registered for alerting on orphan accumulation, and a structured warn log is emitted on each drop with the blob key and attempt count.

What this does not address

Why orphan events get published in the first place (likely TTL mismatch between blob lifecycle and NATS retention, or out-of-band cleanup). That belongs in a separate investigation.

Test plan

Run on this branch:

  • go build ./... clean (exit 0)
  • go vet ./... clean (exit 0)
  • go test -race -timeout=60s ./... -count=1 — 79 packages OK, 0 fail, 0 data races. Note: at the Makefile's default -timeout=30s under race-detector parallelism, the unrelated pkg/ingestor/parser/common/scanner package occasionally tips over (it runs ~25–30 s in isolation, with the same behaviour on main); raising the timeout to 60 s clears it.
  • golangci-lint run ./pkg/handler/processor/process/... — 0 issues
  • New unit tests pass: TestNotFoundTracker (per-key increment/clear isolation) and TestDeliveryCountSource (label values). Existing Test_ProcessSubscribe continues to pass unchanged.

…y missing

The ingestor previously retried blob NotFound responses indefinitely:
process.Subscribe returned nil without acking, so NATS redelivered the
same orphaned message forever, generating hundreds of thousands of
GetObject errors per processor over weeks.

After maxBlobNotFoundRetries (3) consecutive NotFound responses for a
message, ack the pubsub message to stop redelivery. Prefer the broker's
native delivery count (NATS JetStream NumDelivered) so the threshold is
shared across replicas and survives processor restarts; fall back to an
in-process per-key counter for backends that do not expose one.

Adds a Prometheus counter guac_processor_blob_notfound_dropped_total
(label processor_id) and structured warn logs so operators can alert on
orphan accumulation. Other Read errors retain previous redelivery
behavior.

Signed-off-by: Daniel Gellman <dgellman8@gmail.com>
@dgellman dgellman force-pushed the fix/blob-nosuchkey-deadletter branch from 31d3007 to 248c530 Compare May 7, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants